<?php

declare(strict_types=1);

namespace DataCube\DataCubeAggregation\Functions\Statistic;

use Brick\Math\BigDecimal;
use Brick\Math\RoundingMode;
use DataCube\DataCubeAggregation\Exception\CustomException;
use DataCube\DataCubeAggregation\Exception\CustomInvalidArgumentException;
use Phpml\Math\Statistic\Mean;
use Phpml\Math\Statistic\StandardDeviation;

class StatisticalFunc
{
    public function sampleStandardDeviation(array $sample_array)
    {
        $num = \count($sample_array);
        $sum = array_sum($sample_array);
        $avg = $sum / $num;
        $std = 0;
        for ($i = 0; $i < $num; $i++) {
            $std += pow($sample_array[$i] - $avg, 2);
        }
        return sqrt($std / ($num - 1));
    }

    public function sampleVariance(array $sample_array)
    {
        return StandardDeviation::population($sample_array);
        // $length = \count($sample_array);
        // $mean = array_sum($sample_array) / $length;
        // $variance = 0;
        // foreach ($sample_array as $key => $value) {
        //     $variance += pow($value - $mean, 2);
        // }
        // $variance = $variance / $length;

        // return $variance;
    }

    public function average(array $sample_array, int $scale = 2)
    {
        $len = \count($sample_array);
        if ($len <= 0) {
            throw new CustomException('数值队列为空，请检查原始数据');
        }
        $sum = array_sum($sample_array);

        return BigDecimal::of($sum)->dividedBy($len, $scale, RoundingMode::HALF_UP)->toFloat();
    }

    /**
     * A PHP function that will calculate the median value
     * of an array
     * 
     * @param array $sample The array that you want to get the median value of.
     * @return boolean|float
     * @throws Exception If it's not an array
     */
    public function median(array $sample)
    {
        if (empty($sample)) {
            return false;
        }
        return Mean::median($sample);
    }

    public function sd($x)
    {
        $sd = sqrt($this->variance($x));
        
        return $sd;
    }

    public function variance($x)
    {
        $mean = $this->mean($x);

        $var = 0;

        foreach ($x as $value) {
            $var += ($value - $mean) * ($value - $mean);
        }

        $var = $var / (count($x) - 1);

        return $var;
    }

    public function mean($x, $type="arithmetic")
    {
        $type = strtolower($type);
        
        if ($type == "arithmetic") {
            $total = 0;

            foreach ($x as $value) {
                $total += $value;
            }
            
            $mean = $total/count($x);
        } elseif ($type == "geometric") {
            $total = 1;

            foreach ($x as $value) {
                $total *= $value;
            }
            
            $mean = pow($total, 1/count($x));
        } elseif ($type == "harmonic") {
            $total = 0;

            foreach ($x as $value) {
                $total += 1/$value;
            }
            
            $mean = count($x)/$total;
        }

        return $mean;
    }

    public function arithmetic(array $sample)
    {
        return Mean::arithmetic($sample);
    }

    public function mode(array $sample)
    {
        return Mean::mode($sample);
    }

    public function statsStatPercentile(array $arr, $percentile)
    {
        if (\count($arr) <= 1) {
            return $arr[0];
        }

        if (0 < $percentile && $percentile < 1) {
            $pQuartile = $percentile;
        } elseif (1 <= $percentile && $percentile <= 100) {
            $pQuartile = round($percentile / 100, 3);
        } else {
            return null;
        }

        $pos = (\count($arr) - 1) * $pQuartile;

        // 求百分位数要排序
        sort($arr);

        // 向下
        $base = floor($pos);
        //
        $rest = round($pos, 1) - $base;
        if (!is_float($rest)) {
            return $arr[$pos];
        }
        if (isset($arr[$base + 1])) {
            return $arr[$base] + $rest * ($arr[$base + 1] - $arr[$base]);
        } else {
            return $arr[$base];
        }
    }

    /**
     *
     * Three Sigma Guidelines
     * @return float[]|int[] range
     * */
    public function sigmaRange(array $sample, int $sigma = 3)
    {
        if (empty($sample)) {
            throw new CustomInvalidArgumentException('Data array cannot be empty');
        }
        if (count($sample) === 1) {
            throw new CustomInvalidArgumentException("The array has only 1 element", E_USER_WARNING);
        }
        if ($sigma < 1 || $sigma > 3) {
            throw new CustomInvalidArgumentException('Sigma value must be between 1 and 3');
        }
        $mean = $this->arithmetic($sample);
        $stdDev = $this->sampleStandardDeviation($sample);
        $down = $mean - ($sigma * $stdDev);
        $up = $mean + ($sigma * $stdDev);
        return [$down, $up];
    }

    /**
     * @param array $data
     * @param $upperLimit
     * @param $lowerLimit
     * @return array
     */
    public function identifyOutliers(array $data, $upperLimit, $lowerLimit): array
    {
        $outliers = [];
        foreach ($data as $value) {
            if ($value > $upperLimit || $value < $lowerLimit) {
                $outliers[] = $value;
            }
        }
        return $outliers;
    }
}
